M2.855 · Modelos avanzados de minería de datos · PEC2

2023-1 · Máster universitario en Ciencia de datos (Data science)

Estudios de Informática, Multimedia y Telecomunicación

 

PEC 2: Métodos no supervisados¶

Importante: la entrega debe contener el notebook (.ipynb) y su HTML tras la completa ejecución secuencial (.html) donde se pueda ver el código y los resultados. Para exportar el notebook a HTML puedes hacerlo desde el menú File → Download as → HTML.

A lo largo de esta práctica veremos como aplicar distintas técnicas no supervisadas así como algunas de sus aplicaciones reales:

  • Clustering con distintas estrategias: k-means y regla del codo, basadas en densidad y jerárquicas.
  • Ejemplo práctico con aperturas de ajedrez: reducción de dimensionalidad, clustering y análisis.
Nombre y apellidos: JUAN LARA CHUPS
In [1]:
!pip install umap-learn
!pip install --upgrade hdbscan
Requirement already satisfied: umap-learn in /Users/urimossinger/anaconda3/lib/python3.11/site-packages (0.5.4)
Requirement already satisfied: numpy>=1.17 in /Users/urimossinger/anaconda3/lib/python3.11/site-packages (from umap-learn) (1.24.3)
Requirement already satisfied: scipy>=1.3.1 in /Users/urimossinger/anaconda3/lib/python3.11/site-packages (from umap-learn) (1.11.3)
Requirement already satisfied: scikit-learn>=0.22 in /Users/urimossinger/anaconda3/lib/python3.11/site-packages (from umap-learn) (1.3.0)
Requirement already satisfied: numba>=0.51.2 in /Users/urimossinger/anaconda3/lib/python3.11/site-packages (from umap-learn) (0.58.0)
Requirement already satisfied: pynndescent>=0.5 in /Users/urimossinger/anaconda3/lib/python3.11/site-packages (from umap-learn) (0.5.10)
Requirement already satisfied: tqdm in /Users/urimossinger/anaconda3/lib/python3.11/site-packages (from umap-learn) (4.65.0)
Requirement already satisfied: llvmlite<0.42,>=0.41.0dev0 in /Users/urimossinger/anaconda3/lib/python3.11/site-packages (from numba>=0.51.2->umap-learn) (0.41.0)
Requirement already satisfied: joblib>=0.11 in /Users/urimossinger/anaconda3/lib/python3.11/site-packages (from pynndescent>=0.5->umap-learn) (1.2.0)
Requirement already satisfied: threadpoolctl>=2.0.0 in /Users/urimossinger/anaconda3/lib/python3.11/site-packages (from scikit-learn>=0.22->umap-learn) (2.2.0)
Requirement already satisfied: hdbscan in /Users/urimossinger/anaconda3/lib/python3.11/site-packages (0.8.33)
Requirement already satisfied: cython<3,>=0.27 in /Users/urimossinger/anaconda3/lib/python3.11/site-packages (from hdbscan) (0.29.36)
Requirement already satisfied: numpy>=1.20 in /Users/urimossinger/anaconda3/lib/python3.11/site-packages (from hdbscan) (1.24.3)
Requirement already satisfied: scipy>=1.0 in /Users/urimossinger/anaconda3/lib/python3.11/site-packages (from hdbscan) (1.11.3)
Requirement already satisfied: scikit-learn>=0.20 in /Users/urimossinger/anaconda3/lib/python3.11/site-packages (from hdbscan) (1.3.0)
Requirement already satisfied: joblib>=1.0 in /Users/urimossinger/anaconda3/lib/python3.11/site-packages (from hdbscan) (1.2.0)
Requirement already satisfied: threadpoolctl>=2.0.0 in /Users/urimossinger/anaconda3/lib/python3.11/site-packages (from scikit-learn>=0.20->hdbscan) (2.2.0)

Para ello vamos a necesitar las siguientes librerías:

In [2]:
import random

import tqdm
import umap
import numpy as np
import pandas as pd
import sklearn
from sklearn import cluster        # Algoritmos de clustering.
from sklearn import datasets       # Crear datasets.
from sklearn import decomposition  # Algoritmos de reduccion de dimensionalidad.

# Visualizacion.
import matplotlib
import matplotlib.pyplot as plt
import plotly.graph_objects as go
import plotly.express as px

%matplotlib inline

1. Métodos de clustering (4 puntos)¶

Este ejercicio trata de explorar distintas técnicas de agrupamiento ajustándolas a distintos conjuntos de datos.

El objetivo es doble: entender la influencia de los parámetros en su comportamiento, y conocer sus limitaciones en la búsqueda de estructuras de datos.

Generación de los conjuntos de datos¶

In [3]:
X_blobs, y_blobs = datasets.make_blobs(n_samples=1000, n_features=2, centers=4, cluster_std=1.6, random_state=42)
X_moons, y_moons = datasets.make_moons(n_samples=1000, noise=.07, random_state=42)
X_circles, y_circles = datasets.make_circles(n_samples=1000, factor=.5, noise=.05, random_state=42)

Cada dataset tiene 2 variables: una variable X que contiene 2 features (columnas) y tantas filas como muestras. Y una variable y que alberga las etiquetas que identifican cada cluster.

A lo largo del ejercicio no se usará la variable y (sólo con el objetivo de visualizar). El objetivo es a través de los distintos modelos de clustering conseguir encontrar las estructuras descritas por las variables y.

In [4]:
fig, axis = plt.subplots(2, 3, figsize=(13, 8))
for i, (X, y, ax, name) in enumerate(zip([X_blobs, X_moons, X_circles] * 2,
                                         [None] * 3 + [y_blobs, y_moons, y_circles],
                                         axis.reshape(-1),
                                         ['Blob', 'Moons', 'Circles'] * 2)):
    ax.set_title('Dataset: {}, '.format(name) + ('lo que analizarás' if i < 3 else 'los grupos a encontrar'))
    ax.scatter(X[:,0], X[:,1], s=15, c=y, alpha=.3)
    ax.axis('equal')
plt.tight_layout()

1 a. K-means¶

En este apartado se pide probar el algoritmo k-means sobre los tres datasets presentados anteriormente ajustando con los parámetros adecuados y analizar sus resultados.

In [5]:
X, y = X_blobs, y_blobs

Para estimar el número de clusters a detectar por k-means. Una técnica para estimar $k$ es, como se explica en la teoría:

Los criterios anteriores (minimización de distancias intra grupo o maximización de distancias inter grupo) pueden usarse para establecer un valor adecuado para el parámetro k. Valores k para los que ya no se consiguen mejoras significativas en la homogeneidad interna de los segmentos o la heterogeneidad entre segmentos distintos, deberían descartarse.

Lo que popularmente se conocer como regla del codo.

Primero es necesario calcular la suma de los errores cuadráticos (SSE) que consiste en la suma de todos los errores (distancia de cada punto a su centroide asignado) al cuadrado.

$$SSE = \sum_{i=1}^{K} \sum_{x \in C_i} euclidean(x, c_i)^2$$

Donde $K$ es el número de clusters a buscar por k-means, $x \in C_i$ son los puntos que pertenecen a i-ésimo cluster, $c_i$ es el centroide del cluster $C_i$ (al que pertenece el punto $x$), y $euclidean$ es la distancia euclídea.

Este procedimiento realizado para cada posible valor $k$, resulta en una función monótona decreciente, donde el eje $x$ representa los distintos valores de $k$, y el eje $y$ el $SSE$. Intuitivamente se podrá observar un significativo descenso del error, que indicará el valor idóneo de $k$.

Se pide realizar la representación gráfica de la regla del codo junto a su interpretación, utilizando la librería matplotlib y la implementación en scikit-learn de k-means.

Implementación: cálculo y visualización de la regla del codo en el dataset Blobs.
In [6]:
sse = []
for k in range(1, 11):
    kmeans = cluster.KMeans(n_clusters=k, random_state=42)
    kmeans.fit(X)
    sse.append(kmeans.inertia_)

# Representar gráficamente la regla del codo
plt.figure(figsize=(8, 6))
plt.plot(range(1, 11), sse, marker='o')
plt.title('Regla del Codo para k-means')
plt.xlabel('Número de Clusters (k)')
plt.ylabel('SSE (Suma de Errores Cuadráticos)')
plt.grid(True)
plt.show()
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  super()._check_params_vs_input(X, default_n_init=10)
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  super()._check_params_vs_input(X, default_n_init=10)
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  super()._check_params_vs_input(X, default_n_init=10)
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  super()._check_params_vs_input(X, default_n_init=10)
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  super()._check_params_vs_input(X, default_n_init=10)
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  super()._check_params_vs_input(X, default_n_init=10)
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  super()._check_params_vs_input(X, default_n_init=10)
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  super()._check_params_vs_input(X, default_n_init=10)
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  super()._check_params_vs_input(X, default_n_init=10)
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  super()._check_params_vs_input(X, default_n_init=10)
Análisis: ¿Qué se interpreta en la gráfica? ¿Cómo podría mejorarse la elección de $k$?.

En la grafica se puede observar la representacion de la regla de codo para la eleccion de K cluster para este algoritmo. En el podemos sacar la conclusion de que el mejor numero de cluster es 4 (k=4) debidoa que en el se reduce considerablemente el error y, ademas, no se aprecia una reduccion del error importante al aumentar el numero de clusters.

Al realizar el algoritmo k-means, este se centra en minimizar la distancia intra-grupo utilizando la distancia euclidean. Por lo que, para mejor la eleccion de k se podria utilizar otra forma que incluyera la distancia inter-grupo.

Implementación: cálculo y visualización de los grupos en el dataset Blobs.
In [7]:
# Configurar el gráfico
fig, ax = plt.subplots(1, 1, figsize=(5, 5))

# Configurar y ajustar el modelo K-means
kmeans = cluster.KMeans(n_clusters=4, algorithm='full')
labels = kmeans.fit_predict(X)

# Visualizar los puntos de datos y los centroides
scatter = ax.scatter(X[:, 0], X[:, 1], c=labels, cmap='jet')
centers = ax.scatter(
    kmeans.cluster_centers_[:, 0], kmeans.cluster_centers_[:, 1],
    s=300, c='k', marker='X', linewidths=2, edgecolors='w'
)

# Añadir leyenda
ax.legend([scatter, centers], ['Puntos de Datos', 'Centroides'])

# Configurar el aspecto del gráfico
ax.axis('equal')
plt.tight_layout()

# Mostrar el gráfico
plt.show()
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  super()._check_params_vs_input(X, default_n_init=10)
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1416: FutureWarning: algorithm='full' is deprecated, it will be removed in 1.3. Using 'lloyd' instead.
  warnings.warn(
Análisis: ¿Qué ha sucedido? Explica los motivos por los que crees que se ha producido ese resultado.

En el metodo estandar utilizado de k means se asume la distancia euclidea. Este metodo elige los centroides aleatoriamente y se van ajustando al que minimice el error. Como se toma de referencia el centroide de los cluster, la distancia euclidea asume que los cluster van a tener una forma "circular" por lo que en este caso ha separado bastante bien los datos.

Cabe destacar que en este caso ha sido correcto utilizar esta distancia, pero en la practica podemos encontrarnos con conjuntos de datos para clasifica donde no funcione correctamente o el resultado no sea eficiente de este algoritmo utilizado

In [8]:
X, y = X_moons, y_moons
Implementación: cálculo y visualización de la regla del codo en el dataset Moons.
In [9]:
sse = []
for k in range(1, 11):
    kmeans = cluster.KMeans(n_clusters=k, random_state=42)
    kmeans.fit(X)
    sse.append(kmeans.inertia_)

# Representar gráficamente la regla del codo
plt.figure(figsize=(8, 6))
plt.plot(range(1, 11), sse, marker='o')
plt.title('Regla del Codo para k-means')
plt.xlabel('Número de Clusters (k)')
plt.ylabel('SSE (Suma de Errores Cuadráticos)')
plt.grid(True)
plt.show()
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  super()._check_params_vs_input(X, default_n_init=10)
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  super()._check_params_vs_input(X, default_n_init=10)
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  super()._check_params_vs_input(X, default_n_init=10)
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  super()._check_params_vs_input(X, default_n_init=10)
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  super()._check_params_vs_input(X, default_n_init=10)
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  super()._check_params_vs_input(X, default_n_init=10)
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  super()._check_params_vs_input(X, default_n_init=10)
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  super()._check_params_vs_input(X, default_n_init=10)
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  super()._check_params_vs_input(X, default_n_init=10)
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  super()._check_params_vs_input(X, default_n_init=10)
Análisis: ¿Qué se interpreta en la gráfica? ¿Cómo podría mejorarse la elección de $k$?.

En el caso del dataset moon, podemos observar que no es facil la eleccion de K con el metodo del codo y no coincide con la observacion de los datos al principio del ejercicio donde se esperaban 2 cluster perfectamente diferenciados. Para poder mejorar la eleccion de K se podria utilizar otro metodo como el coeficiente de silueta o utilizar otros metodos diferentes a kmeans.

Implementación: cálculo y visualización de los grupos en el dataset Moons.
In [10]:
# Configurar el gráfico
fig, ax = plt.subplots(1, 1, figsize=(5, 5))

# Configurar y ajustar el modelo K-means
kmeans = cluster.KMeans(n_clusters=2, algorithm='full')
labels = kmeans.fit_predict(X)

# Visualizar los puntos de datos y los centroides
scatter = ax.scatter(X[:, 0], X[:, 1], c=labels, alpha=0.8, cmap='jet')
centers = ax.scatter(
    kmeans.cluster_centers_[:, 0], kmeans.cluster_centers_[:, 1],
    s=350, c='k', marker='X', linewidths=2, edgecolors='w'
)

# Añadir leyenda
ax.legend([scatter, centers], ['Puntos de Datos', 'Centroides'])

# Configurar el aspecto del gráfico
ax.axis('equal')
plt.tight_layout()

# Mostrar el gráfico
plt.show()
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  super()._check_params_vs_input(X, default_n_init=10)
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1416: FutureWarning: algorithm='full' is deprecated, it will be removed in 1.3. Using 'lloyd' instead.
  warnings.warn(
Análisis: ¿Qué ha sucedido? Explica los motivos por los que crees que se ha producido ese resultado.

En este caso, se observar que el algoritmo ha clasificado de forma erronea los datos en dos cluster. Esto se debe a la utilizacion de kmeans con la distancia euclidea ya que este clasifica los datos segun la distancia a los centroides haciendolos con forma circular. En este caso como no tienen forma circular los datos congregados se ha producido el error.

Bloque con sangría

Análisis: Pregunta abierta, ¿qué tendrías que cambiar para conseguir encontrar los grupos en este conjunto de datos?

Para poder encontrar los grupos de este conjunto de datos de la mejor forma se podrian utilizar metricas de distancia como manhattan o chebyshev y probarlos.

De la misma manera, existe otro algoritmo de agrupamiento llamado DBSCAN el cual se basa en la densidad de los datos.

In [11]:
X, y = X_circles, y_circles
Implementación: cálculo y visualización de la regla del codo en el dataset Circles.
In [12]:
sse = []
for k in range(1, 11):
    kmeans = cluster.KMeans(n_clusters=k, random_state=42)
    kmeans.fit(X)
    sse.append(kmeans.inertia_)

# Representar gráficamente la regla del codo
plt.figure(figsize=(8, 6))
plt.plot(range(1, 11), sse, marker='o')
plt.title('Regla del Codo para k-means')
plt.xlabel('Número de Clusters (k)')
plt.ylabel('SSE (Suma de Errores Cuadráticos)')
plt.grid(True)
plt.show()
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  super()._check_params_vs_input(X, default_n_init=10)
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  super()._check_params_vs_input(X, default_n_init=10)
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  super()._check_params_vs_input(X, default_n_init=10)
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  super()._check_params_vs_input(X, default_n_init=10)
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  super()._check_params_vs_input(X, default_n_init=10)
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  super()._check_params_vs_input(X, default_n_init=10)
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  super()._check_params_vs_input(X, default_n_init=10)
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  super()._check_params_vs_input(X, default_n_init=10)
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  super()._check_params_vs_input(X, default_n_init=10)
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  super()._check_params_vs_input(X, default_n_init=10)
Análisis: ¿Qué se interpreta en la gráfica? ¿Cómo podría mejorarse la elección de $k$?.

En este caso ocurre como con el dataset moon, deberia de encontrar que la mejor eleccion de K serian de 2 pero en este caso no se encuentra clara la eleccion de k.

Implementación: cálculo y visualización de los grupos en el dataset Circles.
In [13]:
# Configurar el gráfico
fig, ax = plt.subplots(1, 1, figsize=(5, 5))

# Configurar y ajustar el modelo K-means
kmeans = cluster.KMeans(n_clusters=2, algorithm='full')
labels = kmeans.fit_predict(X)

# Visualizar los puntos de datos y los centroides
scatter = ax.scatter(X[:, 0], X[:, 1], c=labels, alpha=0.8, cmap='jet')
centers = ax.scatter(
    kmeans.cluster_centers_[:, 0], kmeans.cluster_centers_[:, 1],
    s=350, c='k', marker='X', linewidths=2, edgecolors='w'
)

# Añadir leyenda
ax.legend([scatter, centers], ['Puntos de Datos', 'Centroides'])

# Configurar el aspecto del gráfico
ax.axis('equal')
plt.tight_layout()

# Mostrar el gráfico
plt.show()
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  super()._check_params_vs_input(X, default_n_init=10)
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1416: FutureWarning: algorithm='full' is deprecated, it will be removed in 1.3. Using 'lloyd' instead.
  warnings.warn(
Análisis: ¿Qué ha sucedido? Explica los motivos por los que crees que se ha producido ese resultado.

Sucede igual que el anterior, la distancia que se utiliza no es la forma correcta para clasificar estos datos y se deberia de buscar una forma mas eficiente.

1 b. Algoritmos basados en densidad: DBSCAN¶

En este apartado se pide aplicar clustering por densidad como DBSCAN a los datasets anteriores para detectar los dos grupos subyacentes.

Ésta es una visualización intuitiva de su funcionamiento: https://www.youtube.com/watch?v=RDZUdRSDOok

In [14]:
X, y = X_blobs, y_blobs
Implementación: prueba la implementación de DBSCAN en scikit-learn jugando con los parámetros eps y min_samples para encontrar los grupos (y outliers) del dataset Blobs.
In [15]:
mod_dbs = cluster.DBSCAN(eps=2.5, min_samples=140)
clusters = mod_dbs.fit(X)

fig, ax = plt.subplots(1, 1, figsize=(5, 5))
ax.scatter(X[:,0], X[:,1], c=clusters.labels_, alpha=.3, cmap='jet')
ax.axis('equal')
plt.tight_layout()
Análisis: ¿Qué ha sucedido? Explica los motivos por los que crees que se ha producido ese resultado.

Al cambiar el algoritmo de clasificacion a DBSCAN encontramos unos parametros que podemos controlar para encontrar los cluster:

eps: este parametro es significa la disctancia maxima entre dos puntos para que sean considerados vecinos. Se utiliza para controlar la sensibilidad del algoritmo a la densidad.

min_samples: se utiliza para controlar el numero minimo de muestras en la vecindad para que se considere un punto central. Aumentar este parametro requiere que mas puntos esten cerca para formar un cluster.

En este caso se encuentran los 4 clusters utilizando eps aproximado de 2.5 y min samples aproximado de 140 con sus respectivos outliers. Esto se debe a que existe mucha densidad en sus centros (eps) y se encuentran bien separados (min_samples)

In [16]:
X, y = X_moons, y_moons
Implementación: prueba la implementación de DBSCAN jugando con los parámetros eps y min_samples para encontrar los grupos (y outliers) del dataset Moons.
In [17]:
model = cluster.DBSCAN(eps=0.1, min_samples=13, n_jobs=-1)
clusters = model.fit(X)

fig, ax = plt.subplots(1, 1, figsize=(5, 5))
ax.scatter(X[:,0], X[:,1], c=clusters.labels_, alpha=.3, cmap='jet')
ax.axis('equal')
plt.tight_layout()
Análisis: ¿Qué ha sucedido? Explica los motivos por los que crees que se ha producido ese resultado.

En este caso se encuentran los 2 clusters utilizando eps aproximado de 0.1 y min samples aproximado de 13 con sus respectivos outliers. Esto se debe a que existe una densidad constante a lo largo de la forma que construyen y la distancia entre los dos es suficientemente grande.

In [18]:
X, y = X_circles, y_circles
Implementación: prueba la implementación de DBScan jugando con los parámetros eps y min_samples para encontrar los grupos (y outliers) del dataset Circles.
In [19]:
model = cluster.DBSCAN(eps=0.1, min_samples=5, n_jobs=-1)
clusters = model.fit(X)

fig, ax = plt.subplots(1, 1, figsize=(5, 5))
ax.scatter(X[:,0], X[:,1], c=clusters.labels_, alpha=.3, cmap='jet')
ax.axis('equal')
plt.tight_layout()
Análisis: ¿Qué ha sucedido? Explica los motivos por los que crees que se ha producido ese resultado.

En este caso se encuentran los 2 clusters utilizando eps aproximado de 0.1 y min samples aproximado de 5 con sus respectivos outliers. Esto se debe a que existe una densidad constante a lo largo de la forma que construyen y la distancia entre los dos es suficientemente grande. Pasa exactamente igual que con moon.

1 c. Algoritmos jerárquicos¶

En este apartado se pide visualizar mediante un dendrograma la construcción progresiva de los grupos mediante un algoritmo jerárquico aglomerativo (estrategia bottom-up). Con ello se pretende encontrar un método gráfico para entender el comportamiento del algoritmo y encontrar los clusters deseados en cada dataset.

In [20]:
X, y = X_blobs, y_blobs
Implementación: prueba la implementación de clustering jerárquico de scipy probando distintos criterios de enlace o linkage permitiendo identificar los clusters subyacentes (mostrando su resultado) y su dendrograma para el dataset Blobs.
Puedes importar las librerías necesarias para ello.
In [21]:
import matplotlib.pyplot as plt
from scipy.cluster.hierarchy import dendrogram, linkage, fcluster
from scipy.spatial.distance import pdist

# Suponiendo que ya tienes X_blobs definido

# Lista de métodos de enlace a probar
linkage_methods = ['single', 'complete', 'average', 'weighted', 'centroid', 'median', 'ward']

# Iterar sobre los métodos de enlace
for method in linkage_methods:
    # Calcular la matriz de enlace usando el método actual
    linkage_matrix = linkage(pdist(X), method=method)

    # Visualizar el dendrograma
    fig, ax = plt.subplots(1, 2, figsize=(11, 5))
    dendrogram(linkage_matrix, ax=ax[0])
    ax[0].set_title(f'Dataset Blobs con linkage "{method}"')

    # Asignar clusters usando fcluster con el umbral de distancia especificado
    clusters = fcluster(linkage_matrix, t=15, criterion='distance')

    # Visualizar los puntos de datos coloreados por cluster
    ax[1].scatter(X[:, 0], X[:, 1], c=clusters, cmap='jet')
    ax[1].axis('equal')
    ax[1].set_title('Clusters identificados')

    plt.tight_layout()

    # Mostrar el gráfico
    plt.show()
Análisis: Interpreta el dendrograma y comenta qué criterio de enlace se ha comportado mejor. ¿Por qué?

En este casi el mejor enlace es el "complete" debido a que utiliza la maximizacion de las distancia entre los diferentes puntos del cluster, de esta manera consigue distinguir los 4 cluster desde sus centros.

In [22]:
X, y = X_moons, y_moons
Implementación: prueba la implementación de clustering jerárquico de scipy probando distintos criterios de enlace o linkage permitiendo identificar los clusters subyacentes (mostrando su resultado) y su dendrograma para el dataset Moons.
Puedes importar las librerías necesarias para ello.
In [23]:
import matplotlib.pyplot as plt
from scipy.cluster.hierarchy import dendrogram, linkage, fcluster
from scipy.spatial.distance import pdist

# Suponiendo que ya tienes X_blobs definido

# Lista de métodos de enlace a probar
linkage_methods = ['single', 'complete', 'average', 'weighted', 'centroid', 'median', 'ward']

# Iterar sobre los métodos de enlace
for method in linkage_methods:
    # Calcular la matriz de enlace usando el método actual
    linkage_matrix = linkage(pdist(X), method=method)

    # Visualizar el dendrograma
    fig, ax = plt.subplots(1, 2, figsize=(11, 5))
    dendrogram(linkage_matrix, ax=ax[0])
    ax[0].set_title(f'Dataset Moon con linkage "{method}"')

    # Asignar clusters usando fcluster con el umbral de distancia especificado
    clusters = fcluster(linkage_matrix, t=.2, criterion='distance')

    # Visualizar los puntos de datos coloreados por cluster
    ax[1].scatter(X[:, 0], X[:, 1], c=clusters, cmap='jet')
    ax[1].axis('equal')
    ax[1].set_title('Clusters identificados')

    plt.tight_layout()

    # Mostrar el gráfico
    plt.show()
Análisis: Interpreta el dendrograma y comenta qué criterio de enlace se ha comportado mejor. ¿Por qué?

En este caso el enlace simple es que mejor se adapta para poder realizar la division de los dataset. Esto sucede debido a que utilizada la distancia minima entre los puntos mas proximos y encontrar los cluster que presentan formas diferentes a esferas.

In [24]:
X, y = X_circles, y_circles
Implementación: prueba la implementación de clustering jerárquico de scipy probando distintos criterios de enlace o linkage permitiendo identificar los clusters subyacentes (mostrando su resultado) y su dendrograma para el dataset Circles.
Puedes importar las librerías necesarias para ello.
In [25]:
import matplotlib.pyplot as plt
from scipy.cluster.hierarchy import dendrogram, linkage, fcluster
from scipy.spatial.distance import pdist

# Suponiendo que ya tienes X_blobs definido

# Lista de métodos de enlace a probar
linkage_methods = ['single', 'complete', 'average', 'weighted', 'centroid', 'median', 'ward']

# Iterar sobre los métodos de enlace
for method in linkage_methods:
    # Calcular la matriz de enlace usando el método actual
    linkage_matrix = linkage(pdist(X), method=method)

    # Visualizar el dendrograma
    fig, ax = plt.subplots(1, 2, figsize=(11, 5))
    dendrogram(linkage_matrix, ax=ax[0])
    ax[0].set_title(f'Dataset Circles con linkage "{method}"')

    # Asignar clusters usando fcluster con el umbral de distancia especificado
    clusters = fcluster(linkage_matrix, t=0.2, criterion='distance')

    # Visualizar los puntos de datos coloreados por cluster
    ax[1].scatter(X[:, 0], X[:, 1], c=clusters, cmap='jet')
    ax[1].axis('equal')
    ax[1].set_title('Clusters identificados')

    plt.tight_layout()

    # Mostrar el gráfico
    plt.show()
Análisis: Interpreta el dendrograma y comenta qué criterio de enlace se ha comportado mejor. ¿Por qué?

En este caso el enlace simple es que mejor se adapta para poder realizar la division de los dataset. Esto sucede debido a que utilizada la distancia minima entre los puntos mas proximos y encontrar los cluster que presentan formas diferentes a esferas.

2. Ejemplo práctico con aperturas de ajedrez: reducción de dimensionalidad (6 puntos)¶

En ajedrez existen multitud de aperturas y variantes. Te permiten planificar como posicionarás tus piezas, lo cual puede otorgar una gran ventaja durante el desarrollo de la partida. Hay tantas aperturas distintas (cada una de ellas con sus variantes) que puede ser dificil situarte, como puede apreciarse en el siguiente video del Maestro FIDE Luis Fernández.

Como muchas aperturas se parecen a otras, porque tienen planes similares, una buena forma para ubicarse es saber cuales se parecen entre sí. Y esa es la idea de este análisis.

Partiremos de un dataset de partidas de ajedrez en la plataforma lichess, que consta de los siguientes campos (se resaltan los útiles para el análisis):

  • Game ID
  • Rated (T/F)
  • Start Time
  • End Time
  • Number of Turns
  • Game Status
  • Winner
  • Time Increment
  • White Player ID
  • White Player Rating
  • Black Player ID
  • Black Player Rating
  • All Moves in Standard Chess Notation
  • Opening Eco (código estandar de aperturas)
  • Opening Name
  • Opening Ply (número de movimientos de la apertura de la partida)

Se carga el dataset en un dataframe de pandas:

In [26]:
df = pd.read_csv('games.csv')
print(df.head())
         id  rated    created_at  last_move_at  turns victory_status winner  \
0  TZJHLljE  False  1.504210e+12  1.504210e+12     13      outoftime  white   
1  l1NXvwaE   True  1.504130e+12  1.504130e+12     16         resign  black   
2  mIICvQHh   True  1.504130e+12  1.504130e+12     61           mate  white   
3  kWKvrqYL   True  1.504110e+12  1.504110e+12     61           mate  white   
4  9tXo1AUZ   True  1.504030e+12  1.504030e+12     95           mate  white   

  increment_code       white_id  white_rating      black_id  black_rating  \
0           15+2       bourgris          1500          a-00          1191   
1           5+10           a-00          1322     skinnerua          1261   
2           5+10         ischia          1496          a-00          1500   
3           20+0  daniamurashov          1439  adivanov2009          1454   
4           30+3      nik221107          1523  adivanov2009          1469   

                                               moves opening_eco  \
0  d4 d5 c4 c6 cxd5 e6 dxe6 fxe6 Nf3 Bb4+ Nc3 Ba5...         D10   
1  d4 Nc6 e4 e5 f4 f6 dxe5 fxe5 fxe5 Nxe5 Qd4 Nc6...         B00   
2  e4 e5 d3 d6 Be3 c6 Be2 b5 Nd2 a5 a4 c5 axb5 Nc...         C20   
3  d4 d5 Nf3 Bf5 Nc3 Nf6 Bf4 Ng4 e3 Nc6 Be2 Qd7 O...         D02   
4  e4 e5 Nf3 d6 d4 Nc6 d5 Nb4 a3 Na6 Nc3 Be7 b4 N...         C41   

                             opening_name  opening_ply  
0        Slav Defense: Exchange Variation            5  
1  Nimzowitsch Defense: Kennedy Variation            4  
2   King's Pawn Game: Leonardis Variation            3  
3  Queen's Pawn Game: Zukertort Variation            3  
4                        Philidor Defense            5  

2 a. Preparación del dato¶

El primer paso al tratar con dato real es analizar el dato para comprender el dominio, y aplicar determinados filtrados en base a la lógica de tu tarea.

Implementación: filtra (quédate) las partidas que tienen aperturas de 4 o más movimientos (campo opening_ply).
In [27]:
df = df[df['opening_ply'] >= 4]
Implementación: filtra (quédate) las partidas que tienen al menos el doble de movimientos (campo "turns") que los de la apertura (campo opening_ply).
In [28]:
df = df[df['turns'] / 2 > df['opening_ply']]
Implementación: si el nombre de la apertura (campo opening_name) contiene el caracter "|" ignora todo el texto que le sigue.
In [29]:
df['opening_name'] = df['opening_name'].apply(lambda x: x.split('|')[0].strip())
Implementación: filtra (quédate) sólo las aperturas (por su nombre, campo opening_name) que hayan sido usadas en al menos 40 partidas. Para tener un mínimo de muestras de ese tipo, eliminando muchas aperturas apenas usadas.
In [30]:
df_new = df['opening_name'].value_counts() >= 40
df_top = set(df_new.index[df_new])
df = df[df['opening_name'].apply(lambda x: x in df_top)]

AVISO: ¡no es necesario saber interpretar correctamente la notación algebraica para el desarrollo de la práctica!

Los movimientos en ajedrez se pueden transcribir de distintas maneras, la más popular es la notación algebraica. En este caso, la notación algebraica empleada es la inglesa.

En concreto, el campo moves tiene los movimientos alternos tanto de las blancas como de las negras. Por tanto, la secuencia:

e4 c5 Nf3 Qa5...

Se interpretaría como:

  • Las blancas mueven el peón de rey a la casilla e4.
  • Las negras responden con el peón a c5.
  • Las blancas mueven el caballo (N) a la casilla f3.
  • Las negras mueven su dama a la casilla a5.

Y así alternan movimientos hasta el final de la partida (victoria, tablas, abandono o quedarse sin tiempo).

Es importante tener en cuenta que el campo moves es de tipo string, por lo que será necesario dividirlo por el separador (espacio) para tener cada movimiento por separado. Útil para los siguientes pasos.

Implementación: crea una columna llamada "white_moves" que contenga una lista con sólo los movimientos de la apertura del participante blanco (los impares del campo moves y tantos como indique opening_ply).
In [31]:
df['white_moves'] = df.apply(
    lambda x: [m for i, m in enumerate(x.moves.split(' ')) if i % 2 == 0 and i < x.opening_ply * 2],
    axis=1)

Para comparar las aperturas entre sí usaremos los movimientos empleados en ella sólo por parte del jugador blanco (sólo hay nombre de la apertura del jugador blanco). Por simplicidad, podemos ignorar su orden, por lo que podemos usar la estrategia de bag of words. Generando, a partir del campo white_moves un nuevo dataset con tantas dimensiones como posibles movimientos con un 1 si se ha realizado durante la apertura y un 0 si no se ha realizado.

Puedes crear nuevas columnas a partir de valores con el método get_dummies() de pandas.

Si el campo a partir del cual quieres generar las dimensiones es una lista de strings, aquí hay una pista.

Implementación: crea un nuevo dataframe usando el modelo bag of words a partir del campo white_moves. Puedes usar para ello el método get_dummies.
In [32]:
data_bag = df['white_moves'].str.join('|').str.get_dummies()

2 b. Reducción de dimensionalidad¶

En este punto tienes un dataset con tantas filas como partidas y tantas columnas como movimientos posibles efectuados en el conjunto de datos.

El problema es que ahora dispones de muchas dimensiones, por lo que para visualizar los datos y comprobar si hay algún tipo de estructura es necesario reducir su dimensionalidad. Obteniendo un embedding (representación compacta) de las aperturas.

La reducción de dimensionalidad puede llevarse a cabo con métodos como PCA. Pero este método tiene la limitación de que sólo realiza proyecciones lineales. Por lo que otros métodos como t-SNE o UMAP pueden ofrecer mejores resultados.

Implementación: reduce la dimensionalidad de los datos mediante UMAP a dos dimensiones para poder visualizarlo posteriormente.
In [33]:
model = umap.UMAP()
data_map = model.fit_transform(data_bag)
Implementación: visualiza la reducción de dimensionalidad mediante un scatter plot de la librería matplotlib donde cada punto es una muestra con coordenadas obtenidas tras la reducción de dimensionalidad para poder observar las estructuras que han emergido en el nuevo espacio.
In [34]:
import matplotlib.pyplot as plt

# Crear un scatter plot
plt.scatter(data_map[:, 0], data_map[:, 1], c='b', marker='o', s=10)

# Configurar etiquetas y título
plt.title('Visualización UMAP de los Datos en 2D')
plt.xlabel('Dimensión 1')
plt.ylabel('Dimensión 2')

# Mostrar el gráfico
plt.show()
Implementación opcional: visualiza la reducción de dimensionalidad mediante un scatter plot de la librería plotly, al igual que antes, donde cada punto es una muestra con coordenadas obtenidas tras la reducción de dimensionalidad. De tal manera que al situarnos encima de cada muestra con el ratón (hovertext) nos indique el nombre de la apertura usada, para poder explorar mejor los datos.
In [35]:
import plotly.graph_objects as go

fig = go.Figure(
    data=[go.Scatter(
        x=data_map[:, 0],
        y=data_map[:, 1],
        mode='markers',
        hovertext=df['opening_name'],
        marker=dict(
            color='LightSkyBlue',
            size=4,
            opacity=0.2,
        )
    )]
)


fig.update_layout(
    paper_bgcolor='white',
    plot_bgcolor='white',
)

fig.show()

2 c. Clustering¶

Tras observar la estructura de las muestras en baja dimensionalidad.

Análisis: ¿Qué método de clustering sería adecuado para detectar los distintos grupos? ¿Por qué?

Viendo las graficas de los datos puede funcionar el algoritmo HBSCAN, ya que se bansa en la densidad de los datos. Se puede observar que los grupos mantienen diferente tamaño por lo que son irregulares y este algoritmo podria funcionar

Implementación: realiza el clustering con la técnica elegida para que encuentre automáticamente los grupos.
In [36]:
import hdbscan

model = hdbscan.HDBSCAN(min_cluster_size=60)
model.fit(data_map)
model.labels_.max()

model.condensed_tree_.plot()
Out[36]:
<Axes: ylabel='$\\lambda$ value'>
Implementación: visualiza el anterior scatter plot con matplotlib (y opcionalmente con plotly) con la novedad de que el color de cada muestra se corresponda con el cluster asociado para poder observar el efecto del clustering. Utiliza una paleta de colores cualitativa (discreta) para facilitar su interpretación.
In [37]:
fig = go.Figure(
    data=[go.Scatter(x=data_map[model.labels_ >= 0,0],
                     y=data_map[model.labels_ >= 0,1],
                     mode='markers',
                     hovertext=df['opening_name'],
                     marker=dict(
                         color=model.labels_[model.labels_ >= 0],
                         size=5,
                         opacity=0.2,
                         colorscale=px.colors.qualitative.Light24
                     ),
                    )],
    layout=dict(title="Opening's embedding",
                width=900,
                height=800,
                paper_bgcolor='rgba(0,0,0,0)',
                plot_bgcolor='rgba(0,0,0,0)',
                xaxis=dict(
                    linecolor='#cccccc',
                    linewidth=1,
                    zeroline=False,
                    visible=True
                ),
                yaxis=dict(
                    linecolor='#cccccc',
                    linewidth=1,
                    zeroline=False,
                    visible=True
                ),
               ))
fig.show()
Implementación: para cada cluster, visualiza un gráfico de barras donde cada barra sea una de las aperturas que aparecen en el cluster y su tamaño se corresponda con el número de muestras dentro de ese grupo que han realizado esa apertura. Como consejo, puedes usar matplotlib o incluso pandas. También puede ser útil graficar con barras horizontales en lugar de verticales para facilitar su lectura.
In [38]:
fig, axis = plt.subplots(model.labels_.max() + 1, 1, figsize=(10, 100))
for ax, i in zip(axis.squeeze(), range(model.labels_.max() + 1)):
    df[model.labels_ == i]['opening_name'].value_counts().plot(kind='barh', ax=ax, title=f'Cluster {i}')
plt.tight_layout()

2 d. Análisis¶

Una manera habitual de comparar variables (en este caso aperturas) es emplear una matriz de distancias. Donde podemos representar cuál es la distancia mínima, media o máxima entre dos aperturas.

Dado que cada apertura tiene distintas muestras (al menos 40). Si usamos el mínimo y tuviésemos 5 muestras de la apertura A y 3 de la B. La distancia entre las aperturas A y B sería la mínima de las 15 distancias posibles que hay entre las 5 muestras de A y las 3 de B.

Implementación: calcula y visualiza la distancia (euclídea en este caso) entre todas las aperturas mediante una matriz de distancias usando plotly o seaborn (mediante un heatmap). El proceso de cálculo puede llevar mucho tiempo. Se recomienda calcular la distancia entre las muestras en baja dimensionalidad usando la librería numpy empleando la normal tras la resta de las muestras. Si aún así se dilata en el tiempo puedes probar a realizar sampling aleatorio del dataset para reducir el número de muestras. Peinsa que la matriz de distancia tiene bastantes simetrías que pueden ser aprovechadas para ahorrar cálculos. Puedes utilizar tqdm para visualizar y estimar la duración del proceso de cálculo.
In [39]:
import numpy as np
import pandas as pd
from tqdm import tqdm
import plotly.express as px

rnd = 0

# Asumiendo que df es tu DataFrame y que contiene una columna 'opening_name'
# y 'data_map' es un array o una lista de las coordenadas

op2idx = {op: i for i, op in enumerate(df['opening_name'].value_counts().index)}
dists = np.full((len(op2idx), len(op2idx)), np.inf)  # Usamos np.inf como valor inicial para la distancia máxima

# Asumimos que 'data_map' es un arreglo NumPy con las coordenadas de las muestras
# data_map = ... (deberás definir esto según tus datos)

# Iteramos sobre todas las combinaciones posibles de aperturas para calcular la distancia euclidiana mínima
for i, (_, op_a) in tqdm(enumerate(df['opening_name'].items()), total=df.shape[0]):
    for j, (_, op_b) in enumerate(df['opening_name'].items()):
        idx_i = op2idx[op_a]
        idx_j = op2idx[op_b]
        if idx_i == idx_j:
            dists[idx_i, idx_j] = 0.
        elif rnd == 0 or dists[idx_i, idx_j] == np.inf:
            d = np.linalg.norm(data_map[i] - data_map[j])
            dists[idx_i, idx_j] = d
            dists[idx_j, idx_i] = d  # Aprovechamos la simetría de la matriz de distancias
        rnd = (rnd + 1) % 4

# Convertimos la matriz de distancias a un DataFrame para facilitar la visualización
distance_df = pd.DataFrame(dists, index=sorted(op2idx, key=op2idx.get), columns=sorted(op2idx, key=op2idx.get))

# Visualizamos la matriz de distancias con Plotly
fig = px.imshow(distance_df, color_continuous_scale='RdBu', width=1900, height=1900)
fig.show()
100%|██████████████████████████████████████| 6975/6975 [00:39<00:00, 175.87it/s]
Análisis: Argumenta las conclusiones obtenidas tras interpretar las visualizaciones de los puntos anteriores: estructura de las muestras en baja dimensionalidad, clustering, gráficos de la dominancia de las aperturas contenidas en cada cluster y la matriz de distancias.

Después de llevar a cabo el análisis de clustering mediante HDBSCAN y la generación de una matriz de distancias entre clusters, podemos extraer las siguientes conclusiones:

Estructura en Baja Dimensionalidad: La representación visual a través de UMAP revela patrones en nuestros datos. La agrupación en diferentes conjuntos puede indicar diferencias en las características de las muestras, sugiriendo que la reducción de dimensionalidad ha capturado eficientemente la esencia de los datos.

Clustering con HDBSCAN: La implementación de HDBSCAN parece haber sido exitosa al lidiar con la complejidad estructural de nuestros datos, generando grupos bien definidos que destacan las similitudes entre las muestras.

Heatmap: El heatmap exhibe la variabilidad en la proximidad entre clusters. Las disparidades en la intensidad de los colores señalan que la proximidad entre clusters no es uniforme, indicando una estructura de datos rica y posiblemente compleja. Algunos clusters muestran una mayor afinidad entre sí, sugiriendo la existencia de aperturas o estilos de juego específicos.

2 e. OPCIONAL¶

Para comprender mejor lo analizado, puedes visualizar las aperturas de las muestras mediante la librería chess que facilita mucho la tarea.

Para ello selecciona una muestra (más fácil) o un tipo de apertura, y de la columna moves selecciona 2 opening_ply* movimientos de la apertura (el x2 es para coger los de las blancas y las negras). Una vez que ya los tengas puedes crear un tablero con la librería chess:

board = chess.Board()

Tras ello, simplemente itera por esos primeros y pásaselos al tablero mediante el método push_san con:

board.push_san('d4')

hasta agotar los movimientos SOLO de la apertura de blancas y negras en el mismo orden en el que ya aparecen en el campo moves.

In [40]:
!pip install chess
Requirement already satisfied: chess in /Users/urimossinger/anaconda3/lib/python3.11/site-packages (1.10.0)
In [41]:
import chess

def movimientos(df: pd.DataFrame, name: str) -> chess.Board():
    aux = df[df['opening_name'] == name].sample()
    board = chess.Board()
    print(name)
    for x in aux['moves'].values[0].split(' ')[:aux['opening_ply'].values[0] * 2]:
        board.push_san(x)
    return board
In [42]:
movimientos(df, "Queen's Pawn Game: Colle System")
Queen's Pawn Game: Colle System
Out[42]:
r n . q k b . r
p . p b p p p p
. p . . . n . .
. . . p . . . .
. . . P . . . .
. . N . P N . P
P P P . . P P .
R . B Q K B . R
In [ ]: